Skip to content

ena_maps: combine PSET non-spatial dims by coordinate label when projecting to a map - #3633

Open
tmplummer wants to merge 4 commits into
IMAP-Science-Operations-Center:devfrom
tmplummer:3632-ena_maps---combine-pset-non-spatial-dims-by-coordinate-label-when-projecting-to-a-map
Open

tmplummer wants to merge 4 commits into
IMAP-Science-Operations-Center:devfrom
tmplummer:3632-ena_maps---combine-pset-non-spatial-dims-by-coordinate-label-when-projecting-to-a-map

Conversation

@tmplummer

@tmplummer tmplummer commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Change Summary

Overview

Why: project_pset_values_to_map matched only the spatial axis to map pixels; non-spatial dims (e.g. gain_config_id, calibration_prod, esa_energy_step) were combined by array position, with the map variable shaped by the first PSET. A later PSET with different labels, a different order, or a length-1 dim was silently placed by numpy broadcasting — e.g. a PSET with gain_config_id=[1] was added into both gain-config slots of a 2-gain map. Hi L2 gain-config combination (#3619) hits this, and currently works around it by zero-padding every PSET to the full layout, which doubles the projection work and leaves the hazard in place for other callers.

  • project_pset_values_to_map now combines PSET non-spatial dims into the map by coordinate label instead of array position (PUSH and PULL, rectangular and HEALPix maps). A PSET may hold a subset of the map variable's labels, in any order.
  • Mismatched dims, labels not in the map, duplicate labels, or positional size mismatches raise ValueError instead of silently broadcasting.
  • epoch is always combined by position (each PSET has its own epoch label), so Hi and Ultra L2 behavior is unchanged.
  • Map variables can be pre-initialized in data_1d with the full non-spatial coords (pixel dim last). New variables reuse existing map coords for shared dims.
  • Label lookups are resolved once per PSET. The subset-label path is faster than zero-padding PSETs to full labels (~18–23 ms vs ~30 ms per PSET in a local benchmark).

Closes #3632. Adopting this in Hi L2 (#3619) is a follow-up.

Behavior changes

  • A non-spatial dim without coords on the map or PSET is only combined by position when sizes match; a length-1 dim no longer broadcasts across the map (now ValueError). No current caller relies on this, since map variables are created from the first PSET's shape.
  • Coordinate labels are matched exactly. Float labels that differ slightly (e.g. by 1e-12) were previously added by position; they now raise, with the labels printed at full precision. Ultra is unaffected today because it rebuilds its energy coords from constants.
  • The PULL path allocates a zero array of shape (*non_spatial, num_points) per value key per PSET (so it can share the slice-based add with PUSH). Negligible at current Hi/Ultra sizes.

File changes

  • imap_processing/ena_maps/ena_maps.py: new _add_projected_values / _resolve_dim_positions helpers used by both PUSH and PULL; map-variable creation honors existing map dims; PULL masks computed once per PSET; docstring describes label matching and pre-initialization; removed an unreachable value_key check and fixed the value_keys docstring type.
  • imap_processing/tests/ena_maps/test_ena_maps.py: new TestProjectByCoordinateLabel using a small synthetic rectangular PSET.

Testing

  • 35 new tests: subset/reordered labels vs a zero-padded reference (PUSH/PULL × rect/HEALPix × with/without mask), fast-path equivalence, projection order independence, PSET dim order differing from map, each ValueError case (including duplicate labels on the first PSET), coordinate-less dims staying positional, and a full-precision message for near-equal float labels. 21 of the original 25 fail on the old implementation.
  • Existing ena_maps, Hi, and Ultra suites pass unchanged (837 passed, 4 skipped).

tmplummer and others added 2 commits November 6, 2026 09:50
…a map

AbstractSkyMap.project_pset_values_to_map now adds each PSET's binned values
into the map variable by non-spatial coordinate label instead of by array
position, for both PUSH and PULL. PSETs may hold a subset of the map's labels,
in any order. Mismatched dims, unknown labels, and size mismatches raise
ValueError instead of silently broadcasting. The epoch dim is combined by
position. Map variables can be pre-initialized with the full coords.

Closes IMAP-Science-Operations-Center#3632

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Label-not-found error prints labels at full precision and notes that
  labels are matched exactly.
- Positional size-mismatch error states why the dim is positional (no
  coordinate on map/PSET, or epoch).
- Remove unreachable value_key check (KeyError check above covers it).
- Fix value_keys type in project_pset_values_to_map docstring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Duplicate energy-step labels bypass validation, and coordinate-less dimensions gain artificial labels that cause incorrect reordering or unexpected errors during later accumulation.

2 open issues
What changed in this PR

This PR adds coordinate-label matching for non-spatial dimensions in ENA map projection, preparing the shared mapping code for Hi L2 gain-configuration combination without zero-padding each pointing set.

Changes:

  • Aligns subset and reordered labels in PUSH and PULL projection, with positional handling for epoch and unlabeled dimensions.
  • Supports pre-initialized map layouts and adds validation for incompatible dimensions and labels.
  • Adds regression tests across rectangular and HEALPix maps, including masks, projection order, and error cases.
File Description
imap_processing/​tests/​ena_maps/​test_ena_maps.py Adds synthetic pointing-set fixtures and coordinate-label projection tests.
imap_processing/​ena_maps/​ena_maps.py Adds label-resolution and accumulation helpers, updates initialization, and documents projection behavior.

Review effort: Balanced

Comment thread imap_processing/ena_maps/ena_maps.py Resolved
Comment thread imap_processing/ena_maps/ena_maps.py Resolved
…uplicates

- When creating a map variable, copy only real PSET coordinates. Dims
  without a coordinate no longer get artificial 0..N-1 map labels, so they
  stay combined by position.
- Check for duplicate PSET labels before the identical-labels shortcut, so
  duplicates raise even when the map's labels came from the same PSET.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tmplummer tmplummer self-assigned this Nov 6, 2026

@lacoak21 lacoak21 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really good and thorough to me! Just had a few non blocking questions.

Comment thread imap_processing/ena_maps/ena_maps.py Resolved
Comment thread imap_processing/ena_maps/ena_maps.py Outdated
f"Map variable '{value_key}' has dims {map_var.dims}; the last dim "
f"must be the spatial pixel dim '{pixel_dim}'."
)
if set(pset_dims) != set(map_dims) or len(pset_dims) != len(map_dims):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is the len() check needed here? On first look I don't think xarray allows for duplicate dim names.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. Claude says that:

xarray (2025.12.0) it only warns: "We do not yet support duplicate dimension names, but we do allow initial construction"

This can and has been updated to just a single test though.

Comment thread imap_processing/ena_maps/ena_maps.py Outdated
)
if np.array_equal(pset_labels, map_labels):
return None
positions = map_var.indexes[dim].get_indexer(pset_labels)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Good to know about get_indexer().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I learned about that from this PR too 😃

map_labels = map_var.coords[dim].values
# Check before the equality shortcut, so duplicates are caught even when
# the map's labels were copied from this PSET.
if len(np.unique(pset_labels)) != pset_size:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a check anywhere that the map doesn't have duplicate labels?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, there isn't. Great catch! I'll add a check.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ena_maps - Combine PSET non-spatial dims by coordinate label when projecting to a map

3 participants